Skip to content

Leave JBang directives at the top of a file as written - #91

Merged
abashev merged 1 commit into
mainfrom
jbang-directives
Sep 28, 2026
Merged

abashev merged 1 commit into
mainfrom
jbang-directives

Conversation

@abashev

@abashev abashev commented Sep 28, 2026

Copy link
Copy Markdown
Collaborator

Closes #24. The same problem is open upstream as google/google-java-format#1217, raised by JBang's author, and for the first line as google/google-java-format#1215 and google/google-java-format#1218. jbangdev/jbang#1194 asked JBang to accept // DEPS instead and was closed: a formatter should not add a space where there was none.

The bug

JBang reads a script's configuration from line comments with the directive name right after the slashes, and a shell runs the first line when the file is executed. The formatter put a space after the slashes of every line comment and wrapped the long ones:

/// usr/bin/env jbang "$0" "$@" ; exit $?
// JAVA 21+
// DEPS info.picocli:picocli:4.7.6
// DEPS com.fasterxml.jackson.core:jackson-databind:2.17.2 org.slf4j:slf4j-simple:2.0.13
// com.squareup.okhttp3:okhttp:4.12.0 org.jsoup:jsoup:1.18.1
// JAVA_OPTIONS -Xmx512m

JBang no longer saw any of it. The dependencies, the Java version and the options were dropped without a word, and a shell tried to run ///, which is a directory.

Where directives count

JBang's directives reference says a directive "must be in the first comment block of the file (before any code)". Its parser is more lenient: Directives.Extended.getAll reads every line of the file that starts with // at column 0. JBang's own tests rely on that: they put //SOURCES between package and import and //RUNTIME_OPTIONS after the imports.

This change follows the documented rule, which keeps the exemption narrow. A directive after the first line of code is formatted like any other comment, as before.

The fix

JavaCommentsHelper leaves two kinds of line comment exactly as written, with no space after the slashes and no wrapping, when they come before the first token of the file:

  • a directive: one of the 22 names in JBang's Directives.Names, or a name with an integration prefix such as Quarkus's //Q:CONFIG, followed by whitespace or the end of the line;
  • the shell line: the first line of the file, when its first word is a path, as in ///usr/bin/env, //usr/bin/env or the long self-bootstrapping header from JBang's docs.

The helper now takes the JavaInput. Comments are numbered along with tokens, so the index of the first token tells which comments come before any code. Header comments always go through the helper, so Comment.computeFlat(), the path behind #23, is not involved.

What stays as before:

  • //DEPSSS, //deps, //DEPS:x, //JAVA21+, ///DEPS and //TODO get their space everywhere, and JBang ignores them too.
  • // DEPS and // //DEPS are left alone. JBang's own templates switch an optional dependency off with the second form.
  • After package, an import or a class, //DEPS becomes // DEPS and is wrapped like any other comment.

Checked

  • Three goldens, each with its code left unformatted in the input:
    • ojf-issue-24-jbang-script: the script from the issue, plus directives between and after the imports, in the class, after a statement and after the class;
    • ojf-issue-24-jbang-package: the bootstrap header, a license comment, //Q:CONFIG, a tab after the name and a directive after package;
    • ojf-issue-24-jbang-compact-source: //usr/bin/env and a compact source file, gated at JDK 21 like CompactSource.
  • JBangDirectivesTest covers:
    • every directive name;
    • the syntax JBang allows after a name: a tab, a trailing // comment, a ${...} property, no value, and a line longer than 120 columns;
    • the lookalikes above;
    • a shell line after a license comment, and a first line without a path.
  • On main's code the goldens and the 28 kept-as-written cases fail. The ten lookalikes and the two shell-line cases pass on both, since they pin the exemption down.
  • ./gradlew :open-java-format:test on JDK 21: 1597 tests, all green, 12 skipped (the JDK 23+ module import tests).
  • The CLI fixes imports before it formats, and that pass keeps a directive above the imports where it was.
  • The 15,747 files of the JDK 21 sources format exactly as before; none of them has a directive or a shell line.

Version

Only lines that come out broken today, a directive JBang no longer reads or a first line the shell cannot run, format differently. That makes this a bug fix in the 2.x sense, although the output for such files now differs from palantir-java-format 2.98.0.

JBang reads a script's configuration from line comments with the name
right after the slashes, such as //DEPS, //JAVA and //SOURCES, and a
shell runs the first line, ///usr/bin/env jbang "$0" "$@" ; exit $?,
when the file is executed. The formatter put a space after the slashes
of every line comment and wrapped long ones, so JBang silently dropped
the dependencies and options, and the file no longer ran from a shell
(#24, reported upstream as google/google-java-format#1217).

JBang's documentation places directives "in the first comment block of
the file (before any code)". There, JavaCommentsHelper now leaves two
kinds of line comment exactly as written, with no space and no
wrapping: a directive, which is one of the names in JBang's
Directives.Names or a name with an integration prefix such as Quarkus's
//Q:CONFIG, followed by whitespace or the end of the line; and the
first line of the file when its first word is a path, as in
///usr/bin/env, //usr/bin/env or the long self-bootstrapping header.
The helper now takes the JavaInput, whose first token marks where the
code starts, since comments are numbered along with tokens.

After the package declaration, an import or a class, the same text is
an ordinary comment again, and //DEPSSS, //deps or //TODO still get
their space everywhere. JBang's parser is more lenient than its
documentation and reads every line that starts with // at column 0, so
a directive after the imports still stops working; that follows the
documented rule on purpose.

Three goldens cover whole scripts: the one from the issue, one with a
package and a license header, and a compact source file.
JBangDirectivesTest checks every directive name, the syntax JBang
allows after one, and comments that only look like directives. The
15,747 files of the JDK 21 sources format exactly as before; none of
them has a directive or a shell line.
@abashev
abashev merged commit b48a838 into main Sep 28, 2026
16 checks passed
@abashev
abashev deleted the jbang-directives branch September 28, 2026 18:26
abashev added a commit to openjavaformat/docs that referenced this pull request Sep 28, 2026
The JBang change of 2.98.0.5 goes on the list as the bug fix it is,
although code that palantir-java-format has already formatted keeps its
"// DEPS" and does not move: whoever migrates JBang scripts needs to
know. The formatter handled a script's header wrongly. The space after
the slashes hid //DEPS and the other directives from JBang, and the
first line no longer ran from a shell. The item says so, links the JBang
page, and says that spaces an earlier run added have to be removed by
hand.

The corpus paragraph now says the JBang fix changes nothing in the JDK
21 sources, which formatted identically with and without it when the fix
was made (openjavaformat/open-java-format#91).

The GitHub Action page gives 2.98.0.5 as the default of the version
input, as the action's main branch now does
(openjavaformat/open-java-format-action@80a52bb).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Do not rewrite JBang directives: //DEPS becomes // DEPS and the script loses its dependencies

1 participant